// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// package main -- go2cs converted at 2022 March 13 06:29:05 UTC
// Original source: C:\Program Files\Go\src\cmd\doc\dirs.go
namespace go;

using bytes = bytes_package;
using fmt = fmt_package;
using exec = @internal.execabs_package;
using log = log_package;
using os = os_package;
using filepath = path.filepath_package;
using regexp = regexp_package;
using strings = strings_package;
using sync = sync_package;

using semver = golang.org.x.mod.semver_package;


// A Dir describes a directory holding code by specifying
// the expected import path and the file system directory.

using System.Threading;
using System;
public static partial class main_package {

public partial struct Dir {
    public @string importPath; // import path for that dir
    public @string dir; // file system directory
    public bool inModule;
}

// Dirs is a structure for scanning the directory tree.
// Its Next method returns the next Go source directory it finds.
// Although it can be used to scan the tree multiple times, it
// only walks the tree once, caching the data it finds.
public partial struct Dirs {
    public channel<Dir> scan; // Directories generated by walk.
    public slice<Dir> hist; // History of reported Dirs.
    public nint offset; // Counter for Next.
}

private static Dirs dirs = default;

// dirsInit starts the scanning of package directories in GOROOT and GOPATH. Any
// extra paths passed to it are included in the channel.
private static void dirsInit(params Dir[] extra) {
    extra = extra.Clone();

    dirs.hist = make_slice<Dir>(0, 1000);
    dirs.hist = append(dirs.hist, extra);
    dirs.scan = make_channel<Dir>();
    go_(() => dirs.walk(codeRoots()));
}

// Reset puts the scan back at the beginning.
private static void Reset(this ptr<Dirs> _addr_d) {
    ref Dirs d = ref _addr_d.val;

    d.offset = 0;
}

// Next returns the next directory in the scan. The boolean
// is false when the scan is done.
private static (Dir, bool) Next(this ptr<Dirs> _addr_d) {
    Dir _p0 = default;
    bool _p0 = default;
    ref Dirs d = ref _addr_d.val;

    if (d.offset < len(d.hist)) {
        var dir = d.hist[d.offset];
        d.offset++;
        return (dir, true);
    }
    var (dir, ok) = d.scan.Receive();
    if (!ok) {
        return (new Dir(), false);
    }
    d.hist = append(d.hist, dir);
    d.offset++;
    return (dir, ok);
}

// walk walks the trees in GOROOT and GOPATH.
private static void walk(this ptr<Dirs> _addr_d, slice<Dir> roots) {
    ref Dirs d = ref _addr_d.val;

    foreach (var (_, root) in roots) {
        d.bfsWalkRoot(root);
    }    close(d.scan);
}

// bfsWalkRoot walks a single directory hierarchy in breadth-first lexical order.
// Each Go source directory it finds is delivered on d.scan.
private static void bfsWalkRoot(this ptr<Dirs> _addr_d, Dir root) {
    ref Dirs d = ref _addr_d.val;

    root.dir = filepath.Clean(root.dir); // because filepath.Join will do it anyway

    // this is the queue of directories to examine in this pass.
    @string @this = new slice<@string>(new @string[] {  }); 
    // next is the queue of directories to examine in the next pass.
    @string next = new slice<@string>(new @string[] { root.dir });

    while (len(next) > 0) {
        (this, next) = (next, this[(int)0..(int)0]);        foreach (var (_, dir) in this) {
            var (fd, err) = os.Open(dir);
            if (err != null) {
                log.Print(err);
                continue;
            }
            var (entries, err) = fd.Readdir(0);
            fd.Close();
            if (err != null) {
                log.Print(err);
                continue;
            }
            var hasGoFiles = false;
            foreach (var (_, entry) in entries) {
                var name = entry.Name(); 
                // For plain files, remember if this directory contains any .go
                // source files, but ignore them otherwise.
                if (!entry.IsDir()) {
                    if (!hasGoFiles && strings.HasSuffix(name, ".go")) {
                        hasGoFiles = true;
                    }
                    continue;
                } 
                // Entry is a directory.

                // The go tool ignores directories starting with ., _, or named "testdata".
                if (name[0] == '.' || name[0] == '_' || name == "testdata") {
                    continue;
                } 
                // When in a module, ignore vendor directories and stop at module boundaries.
                if (root.inModule) {
                    if (name == "vendor") {
                        continue;
                    }
                    {
                        var (fi, err) = os.Stat(filepath.Join(dir, name, "go.mod"));

                        if (err == null && !fi.IsDir()) {
                            continue;
                        }

                    }
                } 
                // Remember this (fully qualified) directory for the next pass.
                next = append(next, filepath.Join(dir, name));
            }
            if (hasGoFiles) { 
                // It's a candidate.
                var importPath = root.importPath;
                if (len(dir) > len(root.dir)) {
                    if (importPath != "") {
                        importPath += "/";
                    }
                    importPath += filepath.ToSlash(dir[(int)len(root.dir) + 1..]);
                }
                d.scan.Send(new Dir(importPath,dir,root.inModule));
            }
        }
    }
}

private static var testGOPATH = false; // force GOPATH use for testing

// codeRoots returns the code roots to search for packages.
// In GOPATH mode this is GOROOT/src and GOPATH/src, with empty import paths.
// In module mode, this is each module root, with an import path set to its module path.
private static slice<Dir> codeRoots() {
    codeRootsCache.once.Do(() => {
        codeRootsCache.roots = findCodeRoots();
    });
    return codeRootsCache.roots;
}

private static var codeRootsCache = default;

private static bool usingModules = default;

private static slice<Dir> findCodeRoots() {
    slice<Dir> list = default;
    if (!testGOPATH) { 
        // Check for use of modules by 'go env GOMOD',
        // which reports a go.mod file path if modules are enabled.
        var (stdout, _) = exec.Command("go", "env", "GOMOD").Output();
        var gomod = string(bytes.TrimSpace(stdout));

        usingModules = len(gomod) > 0;
        if (usingModules) {
            list = append(list, new Dir(dir:filepath.Join(buildCtx.GOROOT,"src"),inModule:true), new Dir(importPath:"cmd",dir:filepath.Join(buildCtx.GOROOT,"src","cmd"),inModule:true));
        }
        if (gomod == os.DevNull) { 
            // Modules are enabled, but the working directory is outside any module.
            // We can still access std, cmd, and packages specified as source files
            // on the command line, but there are no module roots.
            // Avoid 'go list -m all' below, since it will not work.
            return list;
        }
    }
    if (!usingModules) {
        list = append(list, new Dir(dir:filepath.Join(buildCtx.GOROOT,"src")));
        foreach (var (_, root) in splitGopath()) {
            list = append(list, new Dir(dir:filepath.Join(root,"src")));
        }        return list;
    }
    var (mainMod, vendorEnabled, err) = vendorEnabled();
    if (err != null) {
        return list;
    }
    if (vendorEnabled) { 
        // Add the vendor directory to the search path ahead of "std".
        // That way, if the main module *is* "std", we will identify the path
        // without the "vendor/" prefix before the one with that prefix.
        list = append(new slice<Dir>(new Dir[] { {dir:filepath.Join(mainMod.Dir,"vendor"),inModule:false} }), list);
        if (mainMod.Path != "std") {
            list = append(list, new Dir(importPath:mainMod.Path,dir:mainMod.Dir,inModule:true));
        }
        return list;
    }
    var cmd = exec.Command("go", "list", "-m", "-f={{.Path}}\t{{.Dir}}", "all");
    cmd.Stderr = os.Stderr;
    var (out, _) = cmd.Output();
    foreach (var (_, line) in strings.Split(string(out), "\n")) {
        var i = strings.Index(line, "\t");
        if (i < 0) {
            continue;
        }
        var path = line[..(int)i];
        var dir = line[(int)i + 1..];
        if (dir != "") {
            list = append(list, new Dir(importPath:path,dir:dir,inModule:true));
        }
    }    return list;
}

// The functions below are derived from x/tools/internal/imports at CL 203017.

private partial struct moduleJSON {
    public @string Path;
    public @string Dir;
    public @string GoVersion;
}

private static var modFlagRegexp = regexp.MustCompile("-mod[ =](\\w+)");

// vendorEnabled indicates if vendoring is enabled.
// Inspired by setDefaultBuildMod in modload/init.go
private static (ptr<moduleJSON>, bool, error) vendorEnabled() {
    ptr<moduleJSON> _p0 = default!;
    bool _p0 = default;
    error _p0 = default!;

    var (mainMod, go114, err) = getMainModuleAnd114();
    if (err != null) {
        return (_addr_null!, false, error.As(err)!);
    }
    var (stdout, _) = exec.Command("go", "env", "GOFLAGS").Output();
    var goflags = string(bytes.TrimSpace(stdout));
    var matches = modFlagRegexp.FindStringSubmatch(goflags);
    @string modFlag = default;
    if (len(matches) != 0) {
        modFlag = matches[1];
    }
    if (modFlag != "") { 
        // Don't override an explicit '-mod=' argument.
        return (_addr_mainMod!, modFlag == "vendor", error.As(null!)!);
    }
    if (mainMod == null || !go114) {
        return (_addr_mainMod!, false, error.As(null!)!);
    }
    {
        var (fi, err) = os.Stat(filepath.Join(mainMod.Dir, "vendor"));

        if (err == null && fi.IsDir()) {
            if (mainMod.GoVersion != "" && semver.Compare("v" + mainMod.GoVersion, "v1.14") >= 0) { 
                // The Go version is at least 1.14, and a vendor directory exists.
                // Set -mod=vendor by default.
                return (_addr_mainMod!, true, error.As(null!)!);
            }
        }
    }
    return (_addr_mainMod!, false, error.As(null!)!);
}

// getMainModuleAnd114 gets the main module's information and whether the
// go command in use is 1.14+. This is the information needed to figure out
// if vendoring should be enabled.
private static (ptr<moduleJSON>, bool, error) getMainModuleAnd114() {
    ptr<moduleJSON> _p0 = default!;
    bool _p0 = default;
    error _p0 = default!;

    const @string format = "{{.Path}}\n{{.Dir}}\n{{.GoVersion}}\n{{range context.ReleaseTags}}{{if eq . \"go1.14\"" +
    "}}{{.}}{{end}}{{end}}\n";

    var cmd = exec.Command("go", "list", "-m", "-f", format);
    cmd.Stderr = os.Stderr;
    var (stdout, err) = cmd.Output();
    if (err != null) {
        return (_addr_null!, false, error.As(null!)!);
    }
    var lines = strings.Split(string(stdout), "\n");
    if (len(lines) < 5) {
        return (_addr_null!, false, error.As(fmt.Errorf("unexpected stdout: %q", stdout))!);
    }
    ptr<moduleJSON> mod = addr(new moduleJSON(Path:lines[0],Dir:lines[1],GoVersion:lines[2],));
    return (_addr_mod!, lines[3] == "go1.14", error.As(null!)!);
}

} // end main_package
